﻿using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading;
using Heimdallr.Async;

namespace Heimdallr.IO
{
    /// <summary>
    /// Represents a file system monitor which triggering event whenever incoming file is detected.
    /// </summary>
    public class FileSystemTriggerElement : ServiceElement, ITriggerElement
    {
        #region Field

        internal readonly static ReadOnlyCollection<char> InvalidFileNameChars = new ReadOnlyCollection<char>(Path.GetInvalidFileNameChars());
        internal readonly static ReadOnlyCollection<char> InvalidPathChars = new ReadOnlyCollection<char>(Path.GetInvalidPathChars());

        private readonly string path;
        private readonly string ext;
        private readonly ConcurrentDictionary<string, QueueConsumer<WatcherChangeTypes, FileSystemEventArgs>> missions;  // Key is relative filename. 
        private readonly NameValueCollection mime;

        private int maxReceivedSize; 
        private FileSystemWatcher watcher;

        #endregion

        #region Property

        /// <summary>
        /// Gets or sets the maximum data length of notifications.
        /// A negative value means no upper bound for the data length of incoming notification.
        /// </summary> 
        /// <remarks>
        /// If the value of this property is greater than -1 and <see cref="Notification.ContentLength">ContentLength</see> is above it,
        /// the element will ignore it and will not raise the <see cref="Notify"/> event.
        /// </remarks>
        public int MaxReceivedSize
        {
            get { return this.maxReceivedSize; }
            set { this.maxReceivedSize = value; }
        }

        /// <summary>
        /// Gets the collection which maps file extensions and MIME types and used by this instace.
        /// </summary>
        public NameValueCollection MimeCollection
        {
            get { return this.mime; }
        }

        /// <summary>
        /// Gets the fully qualified path to a directory which is monitored by this instance. 
        /// The default value is <see cref="AppDomain.BaseDirectory"/>.
        /// </summary>
        public string TargetPath
        {
            get { return this.path; }
        }

        /// <summary>
        /// Gets the file extension string used to determine what files are monitored in a directory.
        /// The default value is ".*".
        /// </summary>
        public string FileExtension
        {
            get { return this.ext; }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when a file or directory in <see cref="TargetPath"/> is changed.
        /// </summary>
        public event FireNotificationEventHandler Notify;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance, monitoring current working directory and all files.
        /// </summary>
        public FileSystemTriggerElement()
            : this(string.Empty, ".*")
        {
        }

        /// <summary>
        /// Initializeds a new instance, using specified directory and file extension.
        /// </summary>
        /// <param name="path">The fully qualified or relative path to a directory which is monitored by this instance.</param>
        /// <param name="ext">The file extension string used to determine what files are monitored in a directory.</param>
        /// <exception cref="ArgumentNullException"><paramref name="path"/> or <paramref name="ext"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="path"/> or <paramref name="ext"/> is invalid.</exception>
        public FileSystemTriggerElement(string path, string ext)
        {
            if (path == null)
                throw new ArgumentNullException("path");

            if (path.Length > 0 && InvalidPathChars.Intersect(path).Any())
                throw new ArgumentException("path", "Invalid directory.");

            if (ext == null)
                throw new ArgumentNullException("ext");

            if (ext.Length == 0 || ext.Contains(' ') || ext.FirstOrDefault() != '.')
                throw new ArgumentException("ext", "Invalid extension.");

            if (ext != ".*" && InvalidFileNameChars.Intersect(ext).Any())
                throw new ArgumentException("ext", "Invalid extension.");

            this.path = Path.IsPathRooted(path) ? path : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path);
            this.ext = ext.Trim();

            this.maxReceivedSize = int.MaxValue;
            //this.minReceivedSize = -1;
            this.missions = new ConcurrentDictionary<string, QueueConsumer<WatcherChangeTypes, FileSystemEventArgs>>();
            this.mime = BufferedNotification.GetDefaultMimeCollection();
        }

        #endregion

        #region ServiceElement Implementation

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Stopped</see> to <see cref="ServiceElementStatus">Running</see>.
        /// </summary>
        protected override void OnStart()
        {
            FileSystemWatcher wtchr = this.CreateWatcher();

            wtchr.Created += this.FileSystemWatcher_Created;
            wtchr.Changed += this.FileSystemWatcher_Changed;
            wtchr.Error += this.FileSystemWatcher_Error;
            wtchr.EnableRaisingEvents = true;

            Interlocked.Exchange(ref this.watcher, wtchr);
        }

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Running</see> to <see cref="ServiceElementStatus">Stopped</see>.
        /// </summary>
        protected override void OnStop()
        {
            FileSystemWatcher wtchr = Interlocked.Exchange(ref this.watcher, null);

            if (wtchr == null)
                return;

            wtchr.EnableRaisingEvents = false;
            wtchr.Created -= this.FileSystemWatcher_Created;
            wtchr.Changed -= this.FileSystemWatcher_Changed;
            wtchr.Error -= this.FileSystemWatcher_Error;
            wtchr.Dispose();
        }

        #endregion

        #region FileSystemWatcher Related

        private void FileSystemWatcher_Created(object sender, FileSystemEventArgs e)
        {
            this.missions.GetOrAdd(e.Name, this.QueueConsumerFactory).EnqueueAndStart(e.ChangeType, e);
        }

        private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            this.missions.GetOrAdd(e.Name, this.QueueConsumerFactory).EnqueueAndStart(e.ChangeType, e);
        }

        private void FileSystemWatcher_Error(object sender, ErrorEventArgs e)
        {
            this.OnStop();
            this.OnError(new ExceptionsEventArgs(e.GetException()));
        }

        #endregion

        #region Protected Virtual Method

        /// <summary> 
        /// Executes when the element is creating a <see cref="FileSystemWatcher"/>.
        /// </summary> 
        /// <returns></returns>
        protected virtual FileSystemWatcher CreateWatcher()
        {
            return new FileSystemWatcher(this.path, string.Concat('*', this.ext)) { NotifyFilter = NotifyFilters.Size };
        }

        /// <summary>
        /// Create a new <see cref="FileSystemNotification"/> instance from incoming file name.
        /// </summary>
        /// <param name="filename">Incoming file name includes full path.</param>
        /// <returns>New instance of <see cref="FileSystemNotification"/>.</returns>
        protected virtual Notification CreateNotification(string filename)
        {
            return new FileInfoNotification(NotificationLevel.Info, filename);
        }

        #endregion

        #region Raise Event

        /// <summary>
        /// Raise <see cref="Notify"/> event.
        /// </summary>
        /// <param name="e">Event data for the event.</param>
        /// <remarks>
        /// This method is invoked whenever incoming file is detected 
        /// and the size is below <see cref="MaxReceivedSize"/> properties.
        /// </remarks>
        protected virtual void OnNotify(FireNotificationEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.Notify != null)
                this.Notify.BeginInvoke(this, e, this.Notify.EndInvoke, null);
        }

        #endregion

        #region QueueConsumer Callback & EventHandler

        private QueueConsumer<WatcherChangeTypes, FileSystemEventArgs> QueueConsumerFactory(string fileName)
        {
            QueueConsumer<WatcherChangeTypes, FileSystemEventArgs> result = new QueueConsumer<WatcherChangeTypes, FileSystemEventArgs>(this.QueueConsumerAction);

            result.Completed += this.QueueConsumer_Completed;

            return result;
        }

        private void QueueConsumerAction(WatcherChangeTypes changeType, FileSystemEventArgs e)
        {
            Thread.Sleep(100);
        }

        private void QueueConsumer_Completed(object sender, CompletedEventArgs<FileSystemEventArgs> e)
        {
            QueueConsumer<WatcherChangeTypes, FileSystemEventArgs> queue = null;
            FileSystemEventArgs fse = e.UserState;
            Notification n = this.CreateNotification(fse.FullPath);

            if (n != null && this.maxReceivedSize >= 0 && n.ContentLength < this.maxReceivedSize)
                this.OnNotify(new FireNotificationEventArgs(n));

            if (this.missions.TryRemove(fse.Name, out queue))
                queue.Completed -= this.QueueConsumer_Completed;
        }

        #endregion
    }
}